---
title: 使用安知鱼主题同款的 Twikoo 评论组件
description: "Twikoo 是一个评论组件，支持私有部署和魔改，本篇文章将使用 Docker 部署并复刻安知鱼主题中的评论 CSS 样式"
date: "2024-10-05"
lang: zh
tags:
  - 博客
  - Twikoo
  - 美化
category: 技术
cover: https://cdn.qladgk.com/images/20250508160408279.png
---

import Image from "@/components/mdx/Image";

<Image
  src="https://cdn.qladgk.com/images/20250508160408279.png"
  alt="cover"
  width={999}
  height={527}
  isArticleImage={true}
/>

import CodeGroup from "@/components/mdx/CodeGroup";

## 一、私有部署（Docker）

私有部署涉及终端操作、申请证书、配置反向代理或负载均衡等高级操作，如果对这些不太了解，建议优先选择其他方式部署。

### 1、Docker Compose

```yml
version: "3.8"

services:
  twikoo:
    image: imaegoo/twikoo
    container_name: twikoo
    restart: unless-stopped
    ports:
      - 8888:8080
    environment:
      TWIKOO_THROTTLE: 1000
    volumes:
      - twikoo_data:/app/data

volumes:
  twikoo_data:
    external: true
```

在 8888:8080 中，8888 表示宿主机的端口，8080 表示容器内的端口。这意味着宿主机的 8888 将映射到容器内的 8080 端口，以便外部通过 **ip:8888** 访问容器内的服务。

为了保证数据不丢失，这里使用了外部 **volumes**，默认在 `/var/lib/docker/volumes/twikoo_data`。这允许容器重新创建或更新时，数据仍然保留在宿主机上，而不会被删除或覆盖。

使用 docker compose 运行来创建容器

```bash
docker compose up
```

通过浏览器访问 ip:8888 查询 Twikoo 是否运行成功。

<Image
  src="https://cdn.qladgk.com/images/20241031193904.png"
  alt="Twikoo 运行成功示例"
  width={999}
  height={527}
  isArticleImage={true}
/>

### 2、评论数据的处理

Twikoo 的数据格式为 json 文件，文件位置在容器中的 data 目录，因为使用了 volumes 所以可以在宿主机上找到。

单条评论数据示例：

```json
{
  "_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "uid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "nick": "qlAD",
  "mail": "qlad_adgk@163.com",
  "mailMd5": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "link": "www.qladgk.com",
  "ua": "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36",
  "ip": "12.13.14.15",
  "master": true,
  "url": "/links",
  "href": "https://qladgk.com/links",
  "comment": "<p>已添加，预计今日更新</p>\n",
  "pid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "rid": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "isSpam": false,
  "created": 1111111111111,
  "updated": 1111111111111,
  "ipRegion": "陕西省 XX市 电信",
  "meta": { "revision": 0, "created": 1111111111111, "version": 0 },
  "$loki": 20
}
```

你可以对原 json 数据文件进行修改、导入、导出。

或者使用 Twikoo 的后台管理面板进行数据的操作。

<Image
  src="https://cdn.qladgk.com/images/20241031192415.png"
  alt="Twikoo 管理面板"
  width={999}
  height={527}
  isArticleImage={true}
/>

### 3、后续处理步骤

配置前置代理实现 HTTPS 访问（可以用 Nginx、负载均衡或 Cloudflare 等）

## 二、样式魔改

可以直接去看安知鱼主题的 [GitHub](https://github.com/anzhiyu-c/hexo-theme-anzhiyu) 仓库，里面有 Twikoo 的 styl 文件

CSS 的版本如下：

```css
/* 颜色 */
:root {
  --twikoo-theme-op: #4259ef23;
  --twikoo-white: #fff;
  --twikoo-main: var(--twikoo-theme);
  --twikoo-shadow-black: 0 0 12px 4px rgba(0, 0, 0, 0.05);
  --twikoo-shadow-border: 0 8px 16px -4px #2c2d300c;
  --style-border: 1px solid var(--twikoo-card-border);
  --style-border-hover: 2px solid var(--twikoo-main);
  --style-border-dashed: 1px dashed var(--twikoo-theme-op);
  --style-border-avatar: 4px solid var(--twikoo-background);
  --style-border-always: 2px solid var(--twikoo-card-border);
}

.light {
  --twikoo-theme: #a78bfa;
  --twikoo-theme-op: #a78bfa23;
  --twikoo-green: #57bd6a;
  --twikoo-fontcolor: #363636;
  --twikoo-background: #f7f9fe;
  --twikoo-maskbg: rgba(255, 255, 255, 0.6);
  --twikoo-lighttext: var(--twikoo-main);
  --twikoo-secondtext: rgba(60, 60, 67, 0.6);
  --twikoo-secondbg: #edf0f7;
  --twikoo-card-bg: #fff;
  --twikoo-shadow-lightblack: 0 5px 12px -5px rgba(102, 68, 68, 0);
  --twikoo-card-border: #c0c6d8;
}

.dark {
  --twikoo-theme: #a78bfa;
  --twikoo-theme-op: #a78bfa23;
  --twikoo-green: #57bd6a;
  --twikoo-fontcolor: #f7f7fa;
  --twikoo-background: #18171d;
  --twikoo-maskbg: rgba(0, 0, 0, 0.6);
  --twikoo-lighttext: #f2b94b;
  --twikoo-secondtext: #a1a2b8;
  --twikoo-secondbg: #30343f;
  --twikoo-card-bg: #1d1b26;
  --twikoo-shadow-lightblack: 0 5px 12px -5px rgba(102, 68, 68, 0);
  --twikoo-card-border: #42444a;
}

.OwO .OwO-body {
  min-width: 31.25rem;
}
.twikoo svg {
  color: var(--twikoo-fontcolor);
}
/* 评论区表情放大 */
@keyframes owoIn {
  0% {
    transform: translate(0, -95%);
    opacity: 0;
  }
  100% {
    transform: translate(0, -112%);
    opacity: 1;
  }
}

#owo-big {
  position: fixed;
  align-items: center;
  background-color: rgb(255, 255, 255);
  border: 1px #aaa solid;
  border-radius: 10px;
  z-index: 9999;
  display: none;
  transform: translate(0, -112%);
  overflow: hidden;
  animation: owoIn 0.3s cubic-bezier(0.42, 0, 0.3, 1.11);
}
#owo-big img {
  width: 100%;
}

.tk-expand {
  width: 100%;
  cursor: pointer;
  padding: 0.75em;
  text-align: center;
  transition: all 0.5s;
  border: var(--style-border);
  box-shadow: 0 8px 16px -4px #2c2d300c;
  border-radius: 50px;
  letter-spacing: 5px;
  background-color: var(--twikoo-card-bg);
}

#twikoo .tk-comments > .tk-submit {
  overflow: visible !important;
}
#twikoo .tk-comments .OwO .OwO-body {
  border: var(--style-border-always) !important;
  border-radius: 8px !important;
  overflow: hidden;
  background-color: var(--twikoo-maskbg) !important;
  backdrop-filter: saturate(180%) blur(10px);
  cursor: auto;
  top: 2.1em !important;
  transform: translateZ(0);
  animation: 0.3s ease 0.1s 1 normal both running donate_effcet;
}
#twikoo .tk-comments .OwO .OwO-body .OwO-items-show {
  margin: 12px 8px;
}
#twikoo
  .tk-comments
  button.el-button.tk-cancel.el-button--default.el-button--small {
  background: var(--twikoo-secondbg);
  border-radius: 8px;
  color: var(--twikoo-fontcolor);
}
#twikoo
  .tk-comments
  button.el-button.tk-cancel.el-button--default.el-button--small:hover {
  background: var(--twikoo-lighttext);
  color: var(--twikoo-white);
}
#twikoo .tk-comments a.tk-submit-action-icon.__markdown {
  display: none;
}
#twikoo .tk-comments > div.tk-submit > div.tk-row.actions > a {
  display: none;
}
#twikoo .tk-comments .el-button.tk-preview {
  display: none;
}
#twikoo .tk-comments .el-button--primary.is-disabled,
#twikoo .tk-comments .el-button--primary.is-disabled:active,
#twikoo .tk-comments .el-button--primary.is-disabled:focus,
#twikoo .tk-comments .el-button--primary.is-disabled:hover {
  opacity: 0.2;
}
#twikoo .tk-comments .el-button--primary {
  border-color: var(--twikoo-fontcolor);
  color: var(--twikoo-card-bg);
  border-radius: 12px;
  box-shadow: var(--twikoo-shadow-black);
  transition: 0.3s;
  width: 6.25rem;
  position: absolute;
  top: -43px;
  right: 0;
  margin-left: 0.5rem !important;
  height: 32px;
}
#twikoo .tk-comments .tk-input .el-textarea__inner {
  min-height: 130px !important;
  border-radius: 15px;
  display: block;
  resize: vertical;
  padding: 16px 16px 40px 16px;
  line-height: 1.5;
  box-sizing: border-box;
  width: 100%;
  font-size: inherit;
  color: var(--twikoo-fontcolor);
  background-color: var(--twikoo-secondbg);
  border: var(--style-border-always);
  transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
}

#twikoo .tk-comments .el-input__inner {
  background: var(--twikoo-secondbg) !important;
  border: none !important;
  color: var(--twikoo-fontcolor) !important;
  padding-left: 8px;
}
#twikoo .tk-comments .el-input__inner:focus {
  border: none;
}
#twikoo .tk-comments .el-input-group__append,
#twikoo .tk-comments .el-input-group__prepend {
  background-color: var(--twikoo-card-bg);
  color: var(--twikoo-fontcolor);
  border-color: var(--twikoo-card-border);
  border: none;
  font-weight: 700;
}
#twikoo .tk-comments .el-input-group--prepend .el-input__inner,
#twikoo .tk-comments .el-input-group__append {
  border-radius: 0 10px 10px 0;
}
#twikoo .tk-comments .el-input--small .el-input__inner {
  padding: 8px;
  padding-left: 16px;
}
#twikoo .tk-comments .el-input-group--prepend .el-input__inner,
#twikoo .tk-comments .el-input-group__append {
  border-left-width: 0 !important;
}
#twikoo .tk-comments .tk-meta-input {
  position: relative;
  margin-top: 8px;
  width: calc(100% - 6.875rem);
}
#twikoo
  .tk-comments
  .tk-meta-input
  .el-input.el-input--small.el-input-group.el-input-group--prepend {
  border-radius: 12px;
  background: var(--twikoo-secondbg);
  border: var(--style-border-always);
}
#twikoo .tk-comments .tk-meta-input .el-input .el-input-group__prepend {
  user-select: none;
  border-radius: 12px 0 0 12px;
}
#twikoo
  .tk-comments
  .tk-meta-input
  .el-input--small.el-input-group.el-input-group--prepend:focus-within {
  border: var(--style-border-hover);
}
#twikoo .tk-comments .tk-row .tk-avatar {
  display: none;
}
#twikoo .tk-comments .tk-row .tk-col {
  flex-direction: column-reverse;
}
#twikoo .tk-comments .tk-row.actions {
  margin-bottom: 0;
  margin-left: 0;
  margin-top: 0;
  justify-content: space-around;
}
#twikoo .tk-comments .tk-admin {
  backdrop-filter: blur(5px);
}
#twikoo .tk-comments .el-button {
  background-color: var(--twikoo-fontcolor);
  border: 0 solid var(--twikoo-main);
  color: var(--twikoo-background);
}
#twikoo .tk-comments .tk-tag-green {
  background-color: var(--twikoo-main);
  border: none;
  border-radius: 4px;
  color: var(--twikoo-white);
}
#twikoo .tk-comments .tk-action-icon {
  color: var(--twikoo-main);
  cursor: pointer;
}
#twikoo .tk-comments .tk-icon.__comments {
  color: var(--twikoo-main);
}
#twikoo .tk-comments .tk-actions a {
  cursor: pointer;
}
#twikoo .tk-comments .tk-nick {
  line-height: 40px;
}
#twikoo .tk-comments .tk-extras {
  margin-top: 0.5rem;
  padding-bottom: 0.5rem;
}
#twikoo .tk-comments .tk-expand:hover {
  color: #fff;
  background-color: var(--twikoo-main);
  border: var(--style-border-none);
}

#twikoo .tk-comments .tk-content p {
  margin: 0;
}
#twikoo .tk-comments .tk-admin-config-input .el-input__inner {
  background: transparent !important;
}
#twikoo pre code {
  background: none;
}
#twikoo code {
  padding: 2px 4px;
  background: var(--twikoo-secondbg);
  color: #f47466;
}
#twikoo .tk-comment .tk-submit .tk-avatar,
#twikoo .tk-replies .tk-avatar {
  height: 2.5rem !important;
  width: 2.5rem !important;
}
#twikoo .tk-comment pre {
  background: #272822;
  padding: 1em;
  margin: 0.5em 0;
  overflow: auto;
  border-radius: 0.3em;
}

@media screen and (max-width: 768px) {
  #twikoo .tk-comments-container .tk-comment {
    padding: 1rem;
    border: var(--style-border-always);
    box-shadow: var(--twikoo-shadow-border);
    background: var(--twikoo-card-bg);
  }
  #twikoo .tk-replies .tk-comment {
    border: none;
  }
}

#twikoo .tk-avatar {
  border-radius: 50px;
}
#twikoo .tk-avatar .tk-avatar-img {
  height: 2.5rem !important;
}

#twikoo .tk-replies {
  max-height: 10rem !important;
}
#twikoo .tk-replies.tk-replies-expand {
  max-height: none !important;
}
#twikoo .tk-replies .tk-comment {
  border-top: var(--style-border-dashed);
  border-radius: 12px;
  padding: 1rem 0px 0px;
  margin-top: 0;
  transition: all 0.3s ease 0s;
}
#twikoo .tk-replies .tk-content span:first-child:not(.token) {
  font-size: 0.75rem;
  color: var(--twikoo-secondtext);
}

[data-theme="dark"] #owo-big {
  background-color: #4a4a4a;
}
.tk-comments-container .tk-submit {
  opacity: 1;
  height: auto;
  overflow: visible;
}
/* 输入提示 */
/* 设置文字内容 :nth-child(1)的作用是选择第几个 */
.el-input.el-input--small.el-input-group.el-input-group--prepend:nth-child(
    1
  ):before {
  content: "输入QQ号会自动获取昵称和头像🐧";
}

.el-input.el-input--small.el-input-group.el-input-group--prepend:nth-child(
    2
  ):before {
  content: "收到回复将会发送到您的邮箱📧";
}

.el-input.el-input--small.el-input-group.el-input-group--prepend:nth-child(
    3
  ):before {
  content: "可以通过昵称访问您的网站🔗";
}

/* 当用户点击输入框时显示 */
.el-input.el-input--small.el-input-group.el-input-group--prepend:focus-within::before {
  display: block;
  animation: commonTipsIn 0.3s;
  z-index: 2;
}

.el-input.el-input--small.el-input-group.el-input-group--prepend:focus-within::after {
  display: block;
  animation: commonTriangleIn 0.3s;
}

/* 主内容区 */
.el-input.el-input--small.el-input-group.el-input-group--prepend::before {
  display: none;
  position: absolute;
  top: -60px;
  white-space: nowrap;
  border-radius: 10px;
  left: 50%;
  transform: translate(-50%);
  padding: 14px 18px;
  background: #444;
  color: #fff;
  z-index: 100;
}

/* 小角标 */
.el-input.el-input--small.el-input-group.el-input-group--prepend::after {
  display: none;
  content: "";
  position: absolute;
  border: 12px solid transparent;
  border-top-color: #444;
  left: 50%;
  transform: translate(-50%, -46px);
}

/* 评论框 */
.vwrap {
  box-shadow: 2px 2px 5px #bbb;
  background: rgba(255, 255, 255, 0.3);
  border-radius: 8px;
  padding: 30px;
  margin: 30px 0px 30px 0px;
}

/* 设置评论框 */
.vcard {
  box-shadow: 2px 2px 5px #bbb;
  background: rgba(255, 255, 255, 0.3);
  border-radius: 8px;
  padding: 30px;
  margin: 30px 0px 0px 0px;
}

#twikoo .tk-extra {
  background: var(--twikoo-card-bg);
  border: var(--style-border-always);
  padding: 4px 8px;
  border-radius: 8px;
  margin-right: 4px;
  color: var(--twikoo-secondtext);
  margin-top: 6px;
  font-size: 0.8rem;
}
#twikoo .tk-extra-text {
  font-size: 0.75rem;
}
#twikoo .tk-replies .tk-content {
  font-size: 0.9rem;
}
#twikoo .tk-content {
  margin-top: 0;
}
.tk-content span a:not([data-fancybox="gallery"]) {
  font-weight: 500;
  border-bottom: solid 2px var(--twikoo-lighttext);
  color: var(--twikoo-fontcolor);
  padding: 0 0.2em;
  text-decoration: none;
}
.tk-content span a:not([data-fancybox="gallery"]):hover {
  color: var(--twikoo-white);
  background-color: var(--twikoo-theme);
  border-radius: 4px;
}
.tk-main .tk-content span > a {
  border-bottom: none;
}
#post-comment .comment-head {
  font-size: 0.8em !important;
  margin-bottom: 0.5rem;
}

@keyframes commonTipsIn {
  0% {
    top: -50px;
    opacity: 0;
  }
  100% {
    top: -60px;
    opacity: 1;
  }
}

@keyframes commonTriangleIn {
  0% {
    transform: translate(-50%, -36px);
    opacity: 0;
  }
  100% {
    transform: translate(-50%, -46px);
    opacity: 1;
  }
}
@keyframes donate_effcet {
  0% {
    opacity: 0;
    transform: translateY(-20px);
  }
  100% {
    opacity: 1;
    filter: none;
    transform: translateY(0);
  }
}

#body-wrap.page .el-input__inner {
  background: var(--twikoo-card-bg);
  box-shadow: var(--twikoo-shadow-border);
  color: var(--twikoo-fontcolor);
}
#body-wrap.page .tk-admin-config .el-input__inner {
  color: currentColor;
}

#twikoo.twikoo .el-input__inner:focus,
#twikoo.twikoo .el-textarea__inner:focus {
  /* border-color: var(--twikoo-main); */
}

.tk-comments-container > .tk-comment {
  margin-top: 0 !important;
  margin-bottom: 1rem !important;
  transition: 0.3s;
  border-radius: 12px;
  padding: 0;
  padding-top: 1rem;
  border: none;
  border-top: var(--style-border-dashed);
}

#post-comment .comment-tips {
  background-color: rgba(103, 194, 58, 0.13);
  border: var(--style-border-always);
  border-color: var(--twikoo-green);
  color: var(--twikoo-green);
  border-radius: 8px;
  padding: 8px 12px;
  margin-top: 0.5rem;
  display: none;
  width: 100%;
}

#post-comment .comment-tips.show {
  display: flex;
}

#page .tk-comments-container > .tk-comment {
  background: var(--twikoo-card-bg);
  padding: 1rem;
  padding-bottom: 1rem;
  border: var(--style-border);
  border-top: var(--style-border);
  box-shadow: var(--twikoo-shadow-border);
}
@media (prefers-reduced-motion: no-preference) {
  #page .tk-comments-container > .tk-comment {
    animation: animate-in-and-out 1s linear forwards;
    animation-timeline: view();
  }
  #page .tk-comments-container > .tk-comment:has(.OwO-open) {
    z-index: 1;
  }
}

.tk-content {
  margin-top: 0.5rem;
  overflow: auto;
  max-height: 500px;
}

.tk-comments .tk-row-actions-start {
  position: absolute;
  top: -84px;
  left: 17px;
}

@media screen and (max-width: 768px) {
  .OwO .OwO-body {
    min-width: 260px;
  }
  .tk-comments .tk-row-actions-start {
    top: -176px;
  }
  #twikoo .tk-comments .tk-submit .el-button--primary {
    height: 122px;
    top: -126px;
  }
  #twikoo .el-textarea__inner {
    background: var(--twikoo-card-bg) !important;
    overflow: hidden;
    resize: none !important;
  }

  .tk-comments button.el-button.tk-preview.el-button--default.el-button--small {
    display: none;
  }
  .tk-comments .tk-main .tk-submit .tk-row.actions {
    justify-content: center;
  }
  .tk-comments button.el-button.tk-send,
  .tk-comments button.el-button.tk-cancel {
    width: 100%;
  }
  .tk-comments .tk-row-actions-start {
    position: absolute;
  }
}

.OwO .OwO-body .OwO-items .OwO-item:hover {
  box-shadow: var(--twikoo-shadow-lightblack) !important;
  border-radius: 8px;
}
```

## 三、使用组件（前端部署）

Twikoo 支持的博客主题：https://twikoo.js.org/frontend.html

如果你的博客不支持 Twikoo 则使用 CDN 引入，反之查看你博客对应的官方文档

<CodeGroup variant="file">

```html {copy:true} {footer:true} {title:index.html}
<div id="tcomment"></div>
<script src="https://cdn.jsdelivr.net/npm/twikoo@1.6.39/dist/twikoo.all.min.js"></script>
<script>
  twikoo.init({
    envId: "您的环境id", // 腾讯云环境填 envId；Vercel 环境填地址（https://xxx.vercel.app）
    el: "#tcomment", // 容器元素
    // region: 'ap-guangzhou', // 环境地域，默认为 ap-shanghai，腾讯云环境填 ap-shanghai 或 ap-guangzhou；Vercel 环境不填
    // path: location.pathname, // 用于区分不同文章的自定义 js 路径，如果您的文章路径不是 location.pathname，需传此参数
    // lang: 'zh-CN', // 用于手动设定评论区语言，支持的语言列表 https://github.com/twikoojs/twikoo/blob/main/src/client/utils/i18n/index.js
  });
</script>
```

```tsx {copy:true} {footer:true} {title:index.tsx}
import { useEffect } from "react";

interface TwikooConfig {
  envId: string;
  el: string;
}

interface Twikoo {
  init: (config: TwikooConfig) => void;
}

function TwikooComments() {
  useEffect(() => {
    const script = document.createElement("script");
    script.src =
      "https://cdn.jsdelivr.net/npm/twikoo@1.6.39/dist/twikoo.min.js";
    script.async = true;

    script.onload = () => {
      const { twikoo } = window as unknown as { twikoo: Twikoo };
      twikoo.init({
        envId: "您的环境id",
        el: "#tcomment",
      });
    };

    document.body.appendChild(script);

    return () => {
      document.body.removeChild(script);
    };
  }, []);

  return <div id="tcomment" />;
}

export default TwikooComments;
```

</CodeGroup>
